'use client'; import './style.scss'; import Link from 'next/link'; import { useRouter } from 'next/navigation'; import { useState, useCallback, useRef, FormEvent } from 'react'; import Loading from '@/app/component/Loading'; import { BoardLayout, PostConst } from '@/constants/forum'; import { fetchApi } from '@/lib/utils/client'; import { checkPermission, isBoardAdmin } from '@/lib/utils/permission'; import useAuth from '@/hooks/useAuth'; import useErrorAlert from '@/hooks/useErrorAlert'; import { BoardResponse } from '@/types/response/forum/board'; import { BoardListResponse } from '@/types/response/forum/board'; import { PostResponse } from '@/types/response/forum/post'; import Editor, { Handle } from '../../_component/Editor'; import HeaderContent from '../../_component/HeaderContent'; import FooterContent from '../../_component/FooterContent'; import PostTagInput from '../../_component/PostTagInput'; type Props = { _boardList: BoardListResponse, _board: BoardResponse, _post: PostResponse }; export default function View({ _boardList, _board, _post }: Props) { const router = useRouter(); const { member } = useAuth(); const { setError } = useErrorAlert(); const [loading, setLoading] = useState(false); const [isChanged, setIsChanged] = useState(false); const [board, setBoard] = useState(_board); const [boardCode, setBoardCode] = useState(_post.boardCode); const [boardPrefixID, setBoardPrefixID] = useState(_post.boardPrefixID?.toString() ?? ''); const [subject, setSubject] = useState(_post.subject); const [content, setContent] = useState(_post.content); const [isSecret, setIsSecret] = useState(_post.isSecret); const [isNotice, setIsNotice] = useState(_post.isNotice); const [isSpeaker, setIsSpeaker] = useState(_post.isSpeaker); const [tags, setTags] = useState(_post.tagList.map((tag) => tag.slug)); const editorRef = useRef(null); const boardCodeRef = useRef(null); const boardPrefixIDRef = useRef(null); const subjectRef = useRef(null); const contentRef = useRef(null); const redirectUrl = `/post/${_post.id}`; // 게시글 초기화 (DB 데이터로 복원) const resetForm = () => { setIsChanged(false); setBoardPrefixID(_post.boardPrefixID?.toString() ?? ''); setSubject(_post.subject); setIsSecret(_post.isSecret); setIsNotice(_post.isNotice); setIsSpeaker(_post.isSpeaker); setTags(_post.tagList.map((tag) => tag.slug)); const originalContent = _post.content; setContent(originalContent); // Editor 초기화 if (editorRef.current?.editorInstance) { editorRef.current.editorInstance.setData(originalContent); } }; // 게시판 선택 시 const handleBoardChange = useCallback((e: React.ChangeEvent) => { const code = e.target.value; if (!code) { return; } if (isChanged) { if (!confirm('작성 중인 내용이 사라질 수 있습니다. 게시판을 변경하시겠습니까?')) { return } } setLoading(true); fetchApi('/api/forum/boards/' + code).then((res) => { if (res.success) { setBoardCode(code); setBoard(res.data); setIsChanged(false); resetForm(); } else { throw new Error('게시판을 조회할 수 없습니다.'); } }).catch((err) => { setError(err.message); }).finally(() => { setLoading(false); }); }, [isChanged]); // 제목, 내용, 말머리 변경 시 const handleChange = useCallback((e: React.ChangeEvent) => { const { name, value } = e.target as HTMLInputElement|HTMLSelectElement|HTMLTextAreaElement; const checked = (e.target as HTMLInputElement).checked; switch (name) { case 'boardPrefixID': setBoardPrefixID(value); break; case 'isSecret': setIsSecret(checked); break; case 'isNotice': setIsNotice(checked); setIsSpeaker(false); break; case 'isSpeaker': setIsSpeaker(checked); setIsNotice(false); break; case 'subject': setSubject(value); break; case 'content': setContent(value); break; } setIsChanged(true); }, []); // CKEditor에서 내용 변경 시 const handleEditorChange = useCallback((data: string) => { setContent(data); setIsChanged(true); }, []); const validate = useCallback(() => { if (!boardCode || !board) { boardCodeRef.current?.focus(); throw new Error('게시판을 선택해주세요.'); } if (board.boardMeta.write.allowPrefix && board.boardMeta.write.requiredPrefix && !boardPrefixID) { boardPrefixIDRef.current?.focus(); throw new Error((board.boardMeta.list.layout === BoardLayout.QnA ? '분류' : '말머리') + '를 선택해주세요.'); } if (!subject) { subjectRef.current?.focus(); throw new Error('제목을 입력해주세요.'); } else if (subject.length > PostConst.MaxAllowedSubjectLength) { subjectRef.current?.focus(); throw new Error(`제목은 ${PostConst.MaxAllowedSubjectLength}자 이내로 작성해주세요.`); } if (!content) { if (board.boardMeta.write.allowEditor) { editorRef.current?.editorInstance?.editing.view.focus(); } else { contentRef.current?.focus(); } throw new Error('내용을 입력해주세요.'); } else if (!board.boardMeta.write.allowEditor) { // 기본 textarea 사용 시 글자 수 검사 if (content.length > PostConst.MaxAllowedContentLength) { contentRef.current?.focus(); throw new Error(`내용은 ${PostConst.MaxAllowedContentLength}자 이내로 작성해주세요.`); } } if (board.boardMeta.write.allowTag && tags.length > board.boardMeta.write.tagLimit) { throw new Error(`태그는 ${board.boardMeta.write.tagLimit}개 이내로 작성해주세요.`); } }, [boardCode, board, boardPrefixID, subject, content, tags]); // 게시글 수정 처리 const handleSubmit = useCallback(async (e: FormEvent) => { e.preventDefault(); try { validate(); setLoading(true); if (!board) { throw new Error('게시판을 선택해 주세요.'); } const formData = new FormData(); formData.append('postID', _post.id.toString()); formData.append('boardID', board.id.toString()); formData.append('boardCode', boardCode); formData.append('boardPrefixID', boardPrefixID); formData.append('isSecret', isSecret.toString()); formData.append('isNotice', isNotice.toString()); formData.append('isSpeaker', isSpeaker.toString()); formData.append('subject', subject); if (content) { const doc = new DOMParser().parseFromString(content, 'text/html'); doc.querySelectorAll('img[src]').forEach(img => { const src = img.getAttribute('src'); if (src && src.startsWith('data:image/')) { img.setAttribute('src', 'data:image/'); } }); formData.append('content', doc.body.innerHTML); } // 태그 if (board.boardMeta.write.allowTag) { tags.forEach(tag => formData.append('tags', tag)); } // 파일 업로드 권한 체크 const canUploadFile = checkPermission(board.boardMeta, board.boardManager, member).canUploadFile; // 이미지 정보 if (board.boardMeta.write.allowEditor && board.boardMeta.write.allowImage) { const images = editorRef.current?.getImageStore() || []; if (images.length > 0 && !canUploadFile) { throw new Error('이미지를 첨부할 수 있는 권한이 없습니다.'); } images.forEach(i => { if (i.image?.size > 0 && i.name) { formData.append('images', i.image, i.name); } }); } // 미디어 정보 if (board.boardMeta.write.allowEditor && board.boardMeta.write.allowMedia) { const medias = editorRef.current?.getMediaStore() || []; if (medias.length > 0 && !canUploadFile) { throw new Error('영상을 첨부할 수 있는 권한이 없습니다.'); } medias.forEach((m) => { if (m.url) { formData.append('medias', m.url); } }); } // 첨부 파일 if (board.boardMeta.write.allowEditor && board.boardMeta.write.allowFile) { const files = editorRef.current?.getFileStore() || []; if (files.length > 0 && !canUploadFile) { throw new Error('파일을 첨부할 수 있는 권한이 없습니다.'); } files.forEach(f => { if (f?.size > 0 && f.name) { formData.append('files', f.file, f.name); } }); } const res = await fetchApi('/api/forum/posts/' + _post.id, { method: 'PUT', body: formData }); if (res.success) { resetForm(); router.push(redirectUrl); } } catch (err) { if (err instanceof Error) { setError(err.message); } } }, [boardCode, board, boardPrefixID, subject, content, isSecret, isNotice, isSpeaker, tags]); return (
{loading && }

{board?.name} 글 수정

{/* 상단 안내 */} {} {/* 게시판 선택, 말머리, 비밀글, 공지, 전체 공지 */}
{/* 게시판 선택 */}
{/* 말머리 */} {board?.boardMeta.write.allowPrefix && (
)}
{/* 비밀글 */} {board?.boardMeta.write.allowSecret && ( <> )} {isBoardAdmin(board?.boardManager ?? [], member) && ( <> {/* 해당 게시판 공지 */} {!board?.boardMeta.list.exceptNotice && ( <> )} {/* 게시판 전체 공지 */} {!board?.boardMeta.list.exceptSpeaker && ( <> )} )}
{/* 제목 */}
{/* 내용 */}
{board?.boardMeta.write.allowEditor ? ( ) : ( )}
{/* 태그 */} {board?.boardMeta.write.allowTag && (
)} {/* 하단 안내 */} {}

취소
); }